Add preliminary write support for GeoJSON.
authorRobert Lipe <robertlipe@gpsbabel.org>
Thu, 29 Dec 2016 05:50:01 +0000 (00:50 -0500)
committerRobert Lipe <robertlipe@gpsbabel.org>
Thu, 29 Dec 2016 05:50:01 +0000 (00:50 -0500)
Makefile.in
geojson.cc [new file with mode: 0644]
reference/geocaching~json.json [new file with mode: 0644]
reference/track/segmented_tracks~geojson.json [new file with mode: 0644]
testo.d/geojson.test [new file with mode: 0644]
vecs.cc
xmldoc/formats/geojson.xml [new file with mode: 0644]
xmldoc/formats/options/geojson-compact.xml [new file with mode: 0644]

index c7a24b66f2bbc6a571feb8660045eec2bd90c3ca..83af60484bbb4ba49ef3f474851ed7ba78c029bc 100644 (file)
@@ -81,7 +81,7 @@ ALL_FMTS=$(MINIMAL_FMTS) gtm.o gpsutil.o  \
        vpl.o teletype.o jogmap.o bushnell.o bushnell_trl.o wintec_tes.o \
        subrip.o garmin_xt.o garmin_fit.o lowranceusr4.o \
        mtk_locus.o googledir.o mapbar_track.o f90g_track.o mapfactor.o energympro.o \
-       mynav.o ggv_bin.o globalsat_sport.o
+       mynav.o ggv_bin.o globalsat_sport.o geojson.o
 
 FMTS=@FMTS@
 
diff --git a/geojson.cc b/geojson.cc
new file mode 100644 (file)
index 0000000..4ddc233
--- /dev/null
@@ -0,0 +1,169 @@
+/*
+    Copyright (C) 2016 Robert Lipe, robertlipe+source@gpsbabel.org
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111 USA
+
+ */
+#include "defs.h"
+#include <QtCore/QJsonDocument>
+#include <QtCore/QJsonObject>
+#include <QtCore/QJsonArray>
+#include "src/core/file.h"
+
+static gbfile* ofd;
+static const char MYNAME[] = "geojson";
+static char* compact_opt = NULL;
+static QJsonObject* track_object = NULL;
+static QJsonArray* track_coords = NULL;
+
+static arglist_t geojson_args[] = {
+  {"compact", &compact_opt, "Compact Output. Default is off.", 
+    NULL, ARGTYPE_BOOL, ARG_NOMINMAX } ,
+  ARG_TERMINATOR
+};
+
+static void
+geojson_rd_init(const QString& fname) {
+}
+
+QJsonArray* feature_collection = nullptr;
+
+static void
+geojson_wr_init(const QString& fname) {
+  feature_collection = new QJsonArray;
+  ofd = gbfopen(fname, "w", MYNAME);
+}
+
+static void
+geojson_waypt_pr(const Waypoint* waypoint) {
+  QJsonObject object;
+  object["type"]  = "Feature";
+  
+  QJsonObject geometry;
+  geometry["type"] = "Point";
+
+  QJsonArray coords;
+  coords.append(waypoint->longitude);
+  coords.append(waypoint->latitude);
+  if (waypoint->altitude != unknown_alt && waypoint->altitude != 0) {
+    coords.append(waypoint->altitude);
+  }
+
+  geometry["type"] = "Point";
+  geometry["coordinates"] = coords;
+  object["geometry"] = geometry;
+
+  // Build up the properties.
+  QJsonObject properties;
+  if (!waypoint->shortname.isEmpty()) {
+    properties["name"] = waypoint->shortname;
+  }
+  if (!waypoint->description.isEmpty()) {
+    properties["description"] = waypoint->description;
+  }
+  UrlLink link = waypoint->GetUrlLink();
+  if (!link.url_.isEmpty()) {
+    properties["url"] = link.url_;
+  }
+  if (!link.url_link_text_.isEmpty()) {
+    properties["urlname"] = link.url_link_text_;
+  }
+  if (!properties.empty()) {
+    object["properties"] = properties;
+  }
+  
+  feature_collection->append(object);
+}
+
+static void
+geojson_rd_deinit() {
+}
+
+static void
+geojson_wr_deinit(void) {
+  QJsonObject object;
+  object["type"] = "FeatureCollection";
+  object["features"]  = *feature_collection;
+
+  QJsonDocument save(object);
+  QJsonDocument::JsonFormat style;
+  style = compact_opt ? QJsonDocument::Compact : QJsonDocument::Indented;
+  gbfputs(save.toJson(style),ofd);
+
+  gbfclose(ofd);
+  ofd = NULL;
+  delete feature_collection;
+  feature_collection = nullptr;
+}
+
+static void
+geojson_read(void) {
+}
+
+static void geojson_track_hdr(const route_head* track) {
+  track_object = new QJsonObject();
+
+  (*track_object)["type"] = "Feature";
+  track_coords = new QJsonArray();
+
+  QJsonObject properties;
+  if (!track->rte_name.isEmpty()) {
+    properties["name"] = track->rte_name;
+  }
+  (*track_object)["properties"] = properties;
+}
+
+static void geojson_track_disp(const Waypoint* trackpoint) {
+
+  QJsonArray coords;
+  coords.append(trackpoint->longitude);
+  coords.append(trackpoint->latitude);
+  if (trackpoint->altitude != unknown_alt && trackpoint->altitude != 0) {
+    coords.append(trackpoint->altitude);
+  }
+  (*track_coords).append(coords);
+}
+
+static void geojson_track_tlr(const route_head* track) {
+  QJsonObject geometry;
+  geometry["type"] = "LineString";
+  geometry["coordinates"] = *track_coords;
+  (*track_object)["geometry"] = geometry;
+  feature_collection->append(*track_object);
+  delete track_object;
+  track_object = NULL;
+  delete track_coords;
+  track_coords = NULL;
+}
+
+static void
+geojson_write(void) {
+  waypt_disp_all(geojson_waypt_pr);
+  track_disp_all(geojson_track_hdr, geojson_track_tlr, geojson_track_disp);
+}
+
+ff_vecs_t geojson_vecs = {
+  ff_type_file,
+  { (ff_cap)(/*ff_cap_read | */ff_cap_write), ff_cap_write, ff_cap_none },
+  geojson_rd_init,
+  geojson_wr_init,
+  geojson_rd_deinit,
+  geojson_wr_deinit,
+  geojson_read,
+  geojson_write,
+  NULL,
+  geojson_args,
+  CET_CHARSET_UTF8, 0  /* CET-REVIEW */
+};
diff --git a/reference/geocaching~json.json b/reference/geocaching~json.json
new file mode 100644 (file)
index 0000000..f82db55
--- /dev/null
@@ -0,0 +1,149 @@
+{
+    "features": [
+        {
+            "geometry": {
+                "coordinates": [
+                    -87.134699999999995,
+                    35.972033332999999
+                ],
+                "type": "Point"
+            },
+            "properties": {
+                "description": "Mountain Bike Heaven by susy1313",
+                "name": "GCEBB",
+                "url": "http://www.geocaching.com/seek/cache_details.asp?ID=3771",
+                "urlname": "Cache Details"
+            },
+            "type": "Feature"
+        },
+        {
+            "geometry": {
+                "coordinates": [
+                    -86.679550000000006,
+                    36.090683333000001
+                ],
+                "type": "Point"
+            },
+            "properties": {
+                "description": "The Troll by a182pilot & Family",
+                "name": "GC1A37",
+                "url": "http://www.geocaching.com/seek/cache_details.asp?ID=6711",
+                "urlname": "Cache Details"
+            },
+            "type": "Feature"
+        },
+        {
+            "geometry": {
+                "coordinates": [
+                    -86.620116667000005,
+                    35.996266667
+                ],
+                "type": "Point"
+            },
+            "properties": {
+                "description": "Dive Bomber by JoGPS & family",
+                "name": "GC1C2B",
+                "url": "http://www.geocaching.com/seek/cache_details.asp?ID=7211",
+                "urlname": "Cache Details"
+            },
+            "type": "Feature"
+        },
+        {
+            "geometry": {
+                "coordinates": [
+                    -86.648616666999999,
+                    36.038483333000002
+                ],
+                "type": "Point"
+            },
+            "properties": {
+                "description": "FOSTER by JoGPS & Family",
+                "name": "GC25A9",
+                "url": "http://www.geocaching.com/seek/cache_details.asp?ID=9641",
+                "urlname": "Cache Details"
+            },
+            "type": "Feature"
+        },
+        {
+            "geometry": {
+                "coordinates": [
+                    -86.741766666999993,
+                    36.112183332999997
+                ],
+                "type": "Point"
+            },
+            "properties": {
+                "description": "Logan Lighthouse by JoGps & Family",
+                "name": "GC2723",
+                "url": "http://www.geocaching.com/seek/cache_details.asp?ID=10019",
+                "urlname": "Cache Details"
+            },
+            "type": "Feature"
+        },
+        {
+            "geometry": {
+                "coordinates": [
+                    -86.790516667000006,
+                    36.064083332999999
+                ],
+                "type": "Point"
+            },
+            "properties": {
+                "description": "Ganier Cache by Susy1313",
+                "name": "GC2B71",
+                "url": "http://www.geocaching.com/seek/cache_details.asp?ID=11121",
+                "urlname": "Cache Details"
+            },
+            "type": "Feature"
+        },
+        {
+            "geometry": {
+                "coordinates": [
+                    -86.809733332999997,
+                    36.087766666999997
+                ],
+                "type": "Point"
+            },
+            "properties": {
+                "description": "Shy's Hill by FireFighterEng33",
+                "name": "GC309F",
+                "url": "http://www.geocaching.com/seek/cache_details.asp?ID=12447",
+                "urlname": "Cache Details"
+            },
+            "type": "Feature"
+        },
+        {
+            "geometry": {
+                "coordinates": [
+                    -86.891999999999996,
+                    36.057499999999997
+                ],
+                "type": "Point"
+            },
+            "properties": {
+                "description": "GittyUp by JoGPS / Warner Parks",
+                "name": "GC317A",
+                "url": "http://www.geocaching.com/seek/cache_details.asp?ID=12666",
+                "urlname": "Cache Details"
+            },
+            "type": "Feature"
+        },
+        {
+            "geometry": {
+                "coordinates": [
+                    -86.867283333000003,
+                    36.082799999999999
+                ],
+                "type": "Point"
+            },
+            "properties": {
+                "description": "Inlighting by JoGPS / Warner Parks",
+                "name": "GC317D",
+                "url": "http://www.geocaching.com/seek/cache_details.asp?ID=12669",
+                "urlname": "Cache Details"
+            },
+            "type": "Feature"
+        }
+    ],
+    "type": "FeatureCollection"
+}
diff --git a/reference/track/segmented_tracks~geojson.json b/reference/track/segmented_tracks~geojson.json
new file mode 100644 (file)
index 0000000..0dcedc8
--- /dev/null
@@ -0,0 +1,211 @@
+{
+    "features": [
+        {
+            "geometry": {
+                "coordinates": [
+                    [
+                        -86.844139609999999,
+                        35.826145640999997
+                    ],
+                    [
+                        -86.843587434,
+                        35.824858661
+                    ],
+                    [
+                        -86.843868790000002,
+                        35.825508290000002
+                    ],
+                    [
+                        -86.843399026,
+                        35.825659280000004
+                    ],
+                    [
+                        -86.843647121000004,
+                        35.826200632000003
+                    ],
+                    [
+                        -86.843137330000005,
+                        35.825023495000003
+                    ],
+                    [
+                        -86.842846089999995,
+                        35.825764722999999
+                    ],
+                    [
+                        -86.842592054999997,
+                        35.825212972999999
+                    ],
+                    [
+                        -86.842083658999996,
+                        35.825409925000002
+                    ],
+                    [
+                        -86.842527868000005,
+                        35.826530337999998
+                    ],
+                    [
+                        -86.841916886999996,
+                        35.826227992
+                    ],
+                    [
+                        -86.841882385999995,
+                        35.826752995
+                    ],
+                    [
+                        -86.841275397999993,
+                        35.825879004999997
+                    ],
+                    [
+                        -86.840954045999993,
+                        35.826506488
+                    ],
+                    [
+                        -86.840764704999998,
+                        35.826053416000001
+                    ],
+                    [
+                        -86.840254282000004,
+                        35.826187447999999
+                    ],
+                    [
+                        -86.840499359999995,
+                        35.826664997000002
+                    ],
+                    [
+                        -86.840954045999993,
+                        35.826506488
+                    ],
+                    [
+                        -86.839682814,
+                        35.826441473000003
+                    ],
+                    [
+                        -86.840327092999999,
+                        35.827349034999997
+                    ],
+                    [
+                        -86.839510243000007,
+                        35.827133899000003
+                    ],
+                    [
+                        -86.839451225000005,
+                        35.827576071000003
+                    ],
+                    [
+                        -86.838781982,
+                        35.826743954000001
+                    ]
+                ],
+                "type": "LineString"
+            },
+            "properties": {
+                "name": "No Times"
+            },
+            "type": "Feature"
+        },
+        {
+            "geometry": {
+                "coordinates": [
+                    [
+                        -86.844139609999999,
+                        35.836145641000002
+                    ],
+                    [
+                        -86.843587434,
+                        35.834858660999998
+                    ],
+                    [
+                        -86.843868790000002,
+                        35.83550829
+                    ],
+                    [
+                        -86.843399026,
+                        35.835659280000002
+                    ],
+                    [
+                        -86.843647121000004,
+                        35.836200632000001
+                    ],
+                    [
+                        -86.843137330000005,
+                        35.835023495000002
+                    ],
+                    [
+                        -86.842846089999995,
+                        35.835764722999997
+                    ],
+                    [
+                        -86.842592054999997,
+                        35.835212972999997
+                    ],
+                    [
+                        -86.842083658999996,
+                        35.835409925
+                    ],
+                    [
+                        -86.842527868000005,
+                        35.836530338000003
+                    ],
+                    [
+                        -86.841916886999996,
+                        35.836227991999998
+                    ],
+                    [
+                        -86.841882385999995,
+                        35.836752994999998
+                    ],
+                    [
+                        -86.841275397999993,
+                        35.835879005000002
+                    ],
+                    [
+                        -86.840954045999993,
+                        35.836506487999998
+                    ],
+                    [
+                        -86.840764704999998,
+                        35.836053415999999
+                    ],
+                    [
+                        -86.840254282000004,
+                        35.836187447999997
+                    ],
+                    [
+                        -86.840499359999995,
+                        35.836664997
+                    ],
+                    [
+                        -86.840954045999993,
+                        35.836506487999998
+                    ],
+                    [
+                        -86.839682814,
+                        35.836441473000001
+                    ],
+                    [
+                        -86.840327092999999,
+                        35.837349035000003
+                    ],
+                    [
+                        -86.839510243000007,
+                        35.837133899000001
+                    ],
+                    [
+                        -86.839451225000005,
+                        35.837576071000001
+                    ],
+                    [
+                        -86.838781982,
+                        35.836743953999999
+                    ]
+                ],
+                "type": "LineString"
+            },
+            "properties": {
+                "name": "With Times"
+            },
+            "type": "Feature"
+        }
+    ],
+    "type": "FeatureCollection"
+}
diff --git a/testo.d/geojson.test b/testo.d/geojson.test
new file mode 100644 (file)
index 0000000..c4191ef
--- /dev/null
@@ -0,0 +1,10 @@
+#
+# As this format is initially write-only, we're comparing our own "golden"
+# output to what the newly built writer creates. 
+#
+
+gpsbabel -i gpx -f ${REFERENCE}/geocaching.gpx -o geojson -F ${TMPDIR}/geo.json
+compare ${REFERENCE}/geocaching~json.json ${TMPDIR}/geo.json
+
+gpsbabel -i gpx -f ${REFERENCE}/track/segmented_tracks.gpx -o geojson -F ${TMPDIR}/track.json
+compare ${REFERENCE}/track/segmented_tracks~geojson.json ${TMPDIR}/track.json
diff --git a/vecs.cc b/vecs.cc
index 7fde64b3950cc577518b2e47c2949b066f29d9b7..1b739245ce49bca48957c9dd3c76f62bd04cd5ed 100644 (file)
--- a/vecs.cc
+++ b/vecs.cc
@@ -54,6 +54,7 @@ extern ff_vecs_t gcdb_vecs;
 extern ff_vecs_t gdb_vecs;
 extern ff_vecs_t geoniche_vecs;
 extern ff_vecs_t geo_vecs;
+extern ff_vecs_t geojson_vecs;
 extern ff_vecs_t globalsat_sport_vecs;
 extern ff_vecs_t glogbook_vecs;
 extern ff_vecs_t google_dir_vecs;
@@ -1077,6 +1078,13 @@ vecs_t vec_list[] = {
     "trc",
     NULL,
   },
+  {
+    &geojson_vecs,
+    "geojson",
+    "GeoJson",
+    "json",
+    NULL,
+  },
   {
     &ggv_bin_vecs,
     "ggv_bin",
diff --git a/xmldoc/formats/geojson.xml b/xmldoc/formats/geojson.xml
new file mode 100644 (file)
index 0000000..ae1eb44
--- /dev/null
@@ -0,0 +1,31 @@
+<para>
+This module supports a subset of the <ulink url="http://geojson.org/">GeoJSON</ulink> format.
+</para>
+<para>
+GeoJSON is a poor fit for GPSBabel's internal data structures as GPSBabel 
+was designed more around common GPS features (waypoints, tracks, routes) 
+than about GIS-style concepts like MultiPolygons or Geometry Collections.
+In reality, for all but the most simple uses (such as converting a format
+that GPSBabel supports well to something like Leaflet, you should not expect
+high fidelity transfers through this format.
+</para>
+<para>
+Initially, only write support for waypoints and tracks is available.  
+Waypoints are converted to a FeatureCollection of Points.  
+The properties for name and description are written, where available.
+Tracks are converted to a LineString.
+</para>
+<para>
+Potential future work includes the ability to read Point Features and/or
+LineStrings as these would map into our concept of waypoints and 
+routes/tracks.
+The potentially nested/recursive nature of GeoJSON in general would be 
+an awkward implementation.
+</para>
+<para>
+Initial development was free-handed by looking at the <ulink url="https://tools.ietf.org/html/rfc7946 ">GeoJSON RFC</ulink>. Corner cases were handled by
+using <ulink url="http://www.gdal.org/ogr2ogr.html">GDAL's ogr2ogr</ulink>
+to convert GPX to JSON and compare the output. The results were then
+ <ulink url="http://geojsonlint.com/">JSON validated</ulink> and viewed on
+ <ulink url="http://geojson.io/">JSON web viewer</ulink>.
+</para>
diff --git a/xmldoc/formats/options/geojson-compact.xml b/xmldoc/formats/options/geojson-compact.xml
new file mode 100644 (file)
index 0000000..8b13789
--- /dev/null
@@ -0,0 +1 @@
+